Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow subclasses to use _fromObject. #5917

Closed
wants to merge 10 commits into from

Conversation

Harmageddon
Copy link

The default fromObject methods for fabric objects use fabric.Object._fromObject. This works for subclasses, too, if you define the fromObject method like this:

fabric.CustomRect.fromObject = function (object, callback) {
	return fabric.Object._fromObject('CustomRect', object, callback);
};

However, this works only for subclasses within the fabric "namespace". For other namespaces, _fromObject fails to resolve the correct constructor, which means you would have to write your own fromObject method if you have subclasses in other namespaces.

Demo

See the demo: http://jsfiddle.net/5xaq1rz6/5/

Here, we have three kinds of rectangles. The blue one is a normal fabric.Rect, while the other two are instances of subclasses. To make the difference visible, the subclasses only allow their instances to be moved horizontally.
The red rectangle is an instance of fabric.CustomRect, so within the fabric namespace. Clone it by selecting it and clicking the clone button, and you'll see that a new instance of the same subclass is generated correctly.
The green rectangle is an instance of Custom.Rect, a subclass in a different namespace. Clone it, and you'll see that the new instance is a normal rectangle instead of the subclassed one, because the definition for fromObject holds:

Custom.Rect.fromObject = function (object, callback) {
	return fabric.Object._fromObject('Rect', object, callback);
};

So the constructor used for the new object will be fabric.Rect.

Proposed changes

With this change, instead of strings defining the class names, classes themselves can be passed as a className parameter. The code for our rectangle subclass would now be:

Custom.Rect.fromObject = function (object, callback) {
	return fabric.Object._fromObject(Custom.Rect, object, callback);
};

and the object can be reconstructed correctly. For all classes inside the fabric namespace, nothing changes in the usage of fromObject.

@asturur
Copy link
Member

asturur commented Oct 27, 2019

This change would be good for me, but how would we solve full canvas restoration?
In that case classes have still to be appended to fabric in order to be restored.

we have still this utility here that is a big part of the process

    /**
     * Returns klass "Class" object of given namespace
     * @memberOf fabric.util
     * @param {String} type Type of object (eg. 'circle')
     * @param {String} namespace Namespace to get klass "Class" object from
     * @return {Object} klass "Class"
     */
    getKlass: function(type, namespace) {
      // capitalize first letter only
      type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1));
      return fabric.util.resolveNamespace(namespace)[type];
    },

How do you suggest we come back from a full string representation?

@asturur
Copy link
Member

asturur commented Oct 27, 2019

maybe with the use of namespace and adding a namespace property in objects exports it can work, assuming that in 2020 using window and global is still fine for developers.

Let me know if you want to push this forward and if you need some help.

@stale
Copy link

stale bot commented Jan 25, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale Issue marked as stale by the stale bot label Jan 25, 2020
@stale stale bot removed the stale Issue marked as stale by the stale bot label Jan 26, 2020
@Harmageddon
Copy link
Author

How do you suggest we come back from a full string representation?

Good point! I extended the JSFiddle to contain import/export: https://jsfiddle.net/Harmageddon/qwbo6053/
As you suggested, I introduced a "namespace" property and used getKlass in fabric.Object._fromObject to make use of the namespace. But now the toObject tests fail, probably because namespace is not in the reference objects. Do you prefer to add this to the tests or to only set the namespace attribute in toObject when different from null/undefined?

@asturur
Copy link
Member

asturur commented Feb 2, 2020

i ll get back to this, i m currently a bit busy

@Harmageddon
Copy link
Author

Ok, no problem!

@thorstenv
Copy link

i would be very interested in this feature too. Using angular with fabricjs, i dont seem to be able to create custom compontents that can be deserialized (fabric cant find them).

@asturur
Copy link
Member

asturur commented Feb 6, 2020

you can deserialize back if you add them to fabric namespace following the lowerCase schema.

if the class is CustomRect the type has to be set to 'customRect'

@asturur
Copy link
Member

asturur commented Feb 16, 2020

@Harmageddon i see you have changed approach a bit. I would be fine with this change if you can care to document with an example. Does not seem that the default is changed.

@asturur
Copy link
Member

asturur commented Feb 16, 2020

well broken UTs apart.

@asturur
Copy link
Member

asturur commented Feb 16, 2020

What about making a rule? object.type can be something.somethingElse where something would be the namespace and somethingElse would be the class name. In this case if type contains a dot we would apply some rule.
Just an idea.

@Harmageddon
Copy link
Author

@Harmageddon i see you have changed approach a bit. I would be fine with this change if you can care to document with an example. Does not seem that the default is changed.

Yes, as mentioned above, because it seems to be a cleaner solution, and because getKlass already supports a namespace argument.

well broken UTs apart.

That's because of the toObject method being changed, as I mentioned here:

But now the toObject tests fail, probably because namespace is not in the reference objects. Do you prefer to add this to the tests or to only set the namespace attribute in toObject when different from null/undefined?

So either we add the namespace property to the expected value in the unit tests (if we want the namespace property to be exported always), or we just don't export it in toObject when unset, and maybe add a unit test for custom namespaces.

What about making a rule? object.type can be something.somethingElse where something would be the namespace and somethingElse would be the class name. In this case if type contains a dot we would apply some rule.
Just an idea.

That's also possible, would just need to change getKlass then. But then what to do for getKlass('Custom.Rect', 'SomeOtherNamespace')?

For my use case, I provided an example with the fiddle at https://jsfiddle.net/Harmageddon/qwbo6053/, but would be happy to provide more documentation if wanted.

@asturur
Copy link
Member

asturur commented Feb 16, 2020

i think if we export the namespace only when is different from falsy and we make test pass, plus we add a test with a custom namespace, we are good to go.

@Harmageddon
Copy link
Author

I modified the toObject to only include namespace if not falsy and added two tests: toObject with namespace and clone with namespace. The latter fails in the node environment, because window is not available. @asturur do you have an idea how this test has to look in order to work?

@asturur
Copy link
Member

asturur commented Feb 23, 2020

you can try to swap window with document.defaultView, that should help to pass

@Harmageddon Harmageddon force-pushed the clone_subclassed branch 3 times, most recently from caf2696 to f30c782 Compare February 27, 2020 15:13
@Harmageddon
Copy link
Author

Somehow I got Github to hiccup by altering the commit history here. I can create a new PR or maybe it can be solved when merging by squashing.
However, neither using document.defaultView nor global worked. When I create global.Custom, it works within the test, but inside resolveNamespace, Custom seems to be undefined.

@asturur
Copy link
Member

asturur commented Feb 27, 2020

merge + squash should do it.

@asturur
Copy link
Member

asturur commented Feb 27, 2020

so i see there is will to solve here. i do not want to have this stopped forever.
Can you please ensure i have write access to the branch on your repo? there is some option around enabling the author to modify the fork. I want to make a couple tries at fixing the test and merge it.

@Harmageddon
Copy link
Author

Thank you! I have enabled "Allow edits from maintainers." for this PR.

@asturur
Copy link
Member

asturur commented Feb 28, 2020

image

The problem is that resolve namespace is wrapped in this statement were global is exports under node.

@asturur
Copy link
Member

asturur commented Feb 28, 2020

diff --git a/src/util/misc.js b/src/util/misc.js
index 969aff64..7856dcd0 100644
--- a/src/util/misc.js
+++ b/src/util/misc.js
@@ -1,4 +1,4 @@
-(function(global) {
+(function() {
 
   var sqrt = Math.sqrt,
       atan2 = Math.atan2,
@@ -318,7 +318,7 @@
 
       var parts = namespace.split('.'),
           len = parts.length, i,
-          obj = global || fabric.window;
+          obj = typeof global === 'undefined' ? fabric.window : global;
 
       for (i = 0; i < len; ++i) {
         obj = obj[parts[i]];
@@ -975,4 +975,4 @@
       }).join(' ') + ')';
     }
   };
-})(typeof exports !== 'undefined' ? exports : this);
+})();
diff --git a/test/unit/object.js b/test/unit/object.js
index dff18a80..c018aa58 100644
--- a/test/unit/object.js
+++ b/test/unit/object.js
@@ -1,4 +1,4 @@
-(function(){
+(function(global){
 
   var canvas = this.canvas = new fabric.StaticCanvas(null, {enableRetinaScaling: false});
 
@@ -464,21 +464,21 @@
   });
 
   QUnit.test('clone with namespace', function(assert) {
-    var glob = fabric.isLikelyNode ? global : window;
-
-    glob.Custom = {
-      Object: fabric.util.createClass(fabric.Object, {
-        'namespace': 'Custom',
-      })
+    var MyClass = fabric.util.createClass(fabric.Object, {
+      namespace: 'MyNameSpace',
+      type: 'myClass'
+    });
+    MyClass.fromObject = function(object, callback) {
+      return fabric.Object._fromObject('MyClass', object, callback);
     };
-
-    var cObj = new glob.Custom.Object();
-
+    global.MyNameSpace = {
+      MyClass: MyClass
+    };
+    var cObj = new MyClass();
     assert.ok(typeof cObj.clone === 'function');
     cObj.clone(function(clone) {
-      assert.ok(clone instanceof glob.Custom.Object);
-
-      delete glob.Custom;
+      assert.ok(clone instanceof MyClass);
+      delete global.MyNameSpace;
     });
   });
 
@@ -1289,4 +1289,4 @@
     object.fill = 'transparent';
     assert.equal(object.hasFill(), false, 'with a color that is transparent, hasFill is true');
   });
-})();
+})(typeof global !== 'undefined' ? global : this);

@asturur
Copy link
Member

asturur commented Feb 28, 2020

i can't push, unsure what is wrong, that is what i did, could you please merge it in your branch and let's move forward from there.

Co-authored-by: Andrea Bogazzi <andreabogazzi79@gmail.com>
@Harmageddon
Copy link
Author

Awesome, it works! Thank you!

@ShaMan123
Copy link
Contributor

This discussion should be revived coming into v6.
I think we will have to implement something in the direction suggested here.

@ShaMan123
Copy link
Contributor

ShaMan123 commented Jan 22, 2022

I prefer some kind of global registry/map of classes that fabric holds.

fabric.registerClass(type /** id */, clazz)

and/or perhaps another option for overriding while loading from JSON?

and/or have a resolver that the dev can completely override:

fabric.resolveClass = function(type) {
//  return a class constructor
}

@ShaMan123
Copy link
Contributor

ShaMan123 commented Feb 15, 2022

#7657 solves the broader issue and makes this PR stale
especially 9f5ef64

@ShaMan123 ShaMan123 closed this Feb 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants